/*
 * Dijjer - A Peer to Peer HTTP Cache
 * Copyright (C) 2004,2005 Change.Tv, Inc, Mathias De Maré
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package dijjer.io.comm;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import dijjer.AbstractFactory;
import dijjer.Dijjer;

import dijjer.updater.Loader;
import dijjer.updater.ResourceManager;
import dijjer.util.AbstractThread;
import dijjer.util.Misc;
import dijjer.util.VeryLongInteger;
import dijjer.util.logging.Logger;

public abstract class AbstractRoutingTable {

	public static final String VERSION = "$Id: AbstractRoutingTable.java 2 2008-03-21 14:33:03Z ian.clarke $";
	public static final int UCPINGRATE = 5000;
	public static final int PINGRATE = 30000;
	protected AbstractPeer _peer;
	private AbstractPeer _us;
	private final Set _seedNodes;
	private final int _maxSize;
	private final AbstractUdpSocketManager _usm;
	private int _newestSeenBuild = 0;
	private final boolean _isRapidPingDisabled;
	private boolean _allowsLocal;
	private boolean _userNotifiedOfNewsestSeenBuild = false;
	private final ArrayList _uncontactedPeers = new ArrayList();
	private final ArrayList _peers = new ArrayList();
	private final HashMap _lastContact = new HashMap();
	private final HashMap _peerInfo = new HashMap();
	private final LinkedList _changeListeners = new LinkedList();
	private Pinger ucPeerPinger = new Pinger(_uncontactedPeers, UCPINGRATE, 12);
	private Pinger peerPinger = new Pinger(_peers, PINGRATE, 5);
	protected static AbstractRoutingTable _rt;
	private boolean _seedNode;
	protected String _localNetwork;

	public AbstractRoutingTable(int maxSize, final AbstractUdpSocketManager usm, Set seedNodes, boolean noRapidPing,
			boolean allowsLocal, String localNetwork) throws Exception {
		_peer = AbstractFactory.getFactory().getPeer(); // Initially null
		try {
			_us = AbstractFactory.getFactory().getPeer();
		}
		catch(Exception e) {
			Logger.warning("Couldn't add ourselves as peer!");
		}
		_maxSize = maxSize;
		_usm = usm;
		_seedNodes = seedNodes;
		_isRapidPingDisabled = noRapidPing;
		_allowsLocal = allowsLocal;
		peerPinger.startThread();
		if (!noRapidPing) {
			ucPeerPinger.startThread();
		}
		_seedNode = false;
		if (_seedNodes.size() == 0) {
			_seedNode = true;
		}
		_localNetwork = localNetwork;
	}

	public AbstractRoutingTable(int routingTableSize) throws Exception {
		this(routingTableSize, AbstractFactory.getFactory().getUdpSocketManager(), Dijjer.getDijjer().getSeedNodes(), Dijjer
				.getDijjer().isRapidPingDisabled(), Dijjer.getDijjer().getUseLocalNetwork(), Dijjer.getDijjer()
				.getLocalNetwork());
	}

	public void shutdown() {
		removePeers(getPeers(), "Shutting down");
		ucPeerPinger.shutdownThread();
		peerPinger.shutdownThread();
	}
	
	public abstract boolean isUs(AbstractPeer p);

	public void addPeer(AbstractPeer p) {
		if (p == null) {
			Thread.dumpStack();
			Logger.fatal(-1, "Adding null peer");
		}
		if (isUs(p)) {
			Logger.info("Not adding " + p + " to RT because it seems to be us");
			return;
		}
		Logger.info("Adding peer : " + p);
		// Don't add ourselves
		if ((p.equals(_peer))) {
			return;
		}
		if ((_peers.contains(p)) || (_uncontactedPeers.contains(p))) {
			return;
		}
		// If its a seed, assume it works
		if (_seedNodes.contains(p)) {
			_peers.add(p);
		} else {
			_uncontactedPeers.add(p);
		}
		_lastContact.put(p, new Long(System.currentTimeMillis()));
		if (!_seedNode) {
			findLocalPeer(p);
		}
	}

	public AbstractPeer findClosest(VeryLongInteger key) {
		return findClosest(key, null);
	}

	public AbstractPeer findClosest(VeryLongInteger key, HashSet exclude) {
		AbstractPeer closest = null;
		synchronized (_peers) {
			for (Iterator i = _peers.iterator(); i.hasNext();) {
				AbstractPeer cp = (AbstractPeer) i.next();
				if (cp == null || (exclude != null) && (exclude.contains(cp) || exclude.contains(new Integer(cp.hashCode())))) {
					continue;
				}
				if ((closest == null) || (key.closerTo(cp.getHash(), closest.getHash()))) {
					closest = cp;
				}
			}
			if (closest != null) {
				_peers.remove(closest);
				_peers.add(0, closest);
			}
		}
		return closest;
	}

	public void notifyOfContact(AbstractPeer p) {
		if ((!_peers.contains(p)) && (!_uncontactedPeers.contains(p))) {
			return;
		}
		_lastContact.put(p, new Long(System.currentTimeMillis()));
		if (_uncontactedPeers.remove(p)) {
			synchronized (_peers) {
				_peers.add(0, p);
			}
			synchronized (_changeListeners) {
				for (Iterator i1 = _changeListeners.iterator(); i1.hasNext();) {
					ChangeListener cl = (ChangeListener) i1.next();
					cl.added(p);
				}
			}
			while (_peers.size() > _maxSize) {
				AbstractPeer removed;
				synchronized (_peers) {
					removed = (AbstractPeer) _peers.remove(_peers.size() - 1);
				}
				_usm.send(removed, DMT.createDisconnectNotification("Replaced by new peer"));
			}
		}
	}

	public void recontactPeer(AbstractPeer p) {
		synchronized (_peers) {
			if (_peers.remove(p)) {
				if ((!_seedNodes.contains(p)) || _isRapidPingDisabled) {
					_uncontactedPeers.add(p);
				}
			}
		}
		_lastContact.put(p, new Long(System.currentTimeMillis()));
	}

	public void removePeers(List peers, String reason) {
		StringBuffer peerList = new StringBuffer();
		for (Iterator i = peers.iterator(); i.hasNext();) {
			AbstractPeer p = (AbstractPeer) i.next();
			peerList.append(p);
			if (i.hasNext()) {
				peerList.append(", ");
			}
		}
		Logger.info("Removing " + peerList + " (" + peers.size() + ") due to " + reason);
		synchronized (_peers) {
			_peers.removeAll(peers);
		}
		_uncontactedPeers.removeAll(peers);
		_lastContact.keySet().removeAll(peers);
		for (Iterator i = peers.iterator(); i.hasNext();) {
			AbstractPeer peer = (AbstractPeer) i.next();
			if (peer != null) {
				if (_peerInfo.remove(peer) != null) {
					_usm.send(peer, DMT.createDisconnectNotification(reason));
					synchronized (_changeListeners) {
						for (Iterator i1 = _changeListeners.iterator(); i1.hasNext();) {
							ChangeListener cl = (ChangeListener) i1.next();
							cl.removed(peer);
						}
					}
				}
			}
		}
		try { // We test to see whether we have any non-seed peers left, if not we try to reconnect
			Dijjer.getDijjer().connectToNetwork();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public void addChangeListener(ChangeListener cl) {
		synchronized (_changeListeners) {
			_changeListeners.add(cl);
		}
	}

	public void removeChangeListener(ChangeListener cl) {
		synchronized (_changeListeners) {
			_changeListeners.remove(cl);
		}
	}

	public boolean joinSeed(AbstractPeer seed) throws Exception {
		int uid = Misc.nextInt();
		LinkedList exclude = new LinkedList();
		List rtPeers = AbstractFactory.getFactory().getRoutingTable().getPeers();
		synchronized (rtPeers) {
			for (Iterator i = rtPeers.iterator(); i.hasNext();) {
				AbstractPeer p = (AbstractPeer) i.next();
				exclude.add(new Integer(p.hashCode()));
			}
		}
		_usm.send(seed, DMT.createJoinRequest(uid, AbstractFactory.getFactory().getPeer(), Dijjer.JOINTTL, exclude));
		Message ack = _usm.waitFor((MessageFilter.create(10000, DMT.joinRequestAck).setField(DMT.UID, uid)));
		if (ack == null) {
			Logger.error("Seed peer " + seed + " failed to respond to join request after 10 seconds");
			return false;
		}
		Message response;
		if (ack.getSpec().equals(DMT.joinResponse)) {
			response = ack;
		} else {
			response = _usm.waitFor(MessageFilter.create(60000, DMT.joinResponse).setField(DMT.UID, uid));
			if (response == null) {
				Logger.error("Seed peer " + seed
						+ " responded to join request, but failed to provide join response after 30 seconds");
				return false;
			}
		}
		HashSet newPeers = new HashSet((List) response.getObject(DMT.PEERS));
		for (Iterator i = newPeers.iterator(); i.hasNext();) {
			AbstractPeer newPeer = (AbstractPeer) i.next();
			if (newPeer.isNull()) {
				addPeer(response.getSource());
			} else {
				addPeer(newPeer);
			}
		}
		Logger.info("Acquired " + newPeers.size() + " new peers from join request");
		return true;
	}
	
	public abstract boolean sameSubnet(AbstractPeer me, AbstractPeer them);
	
	public void findLocalPeer(AbstractPeer them) {
		AbstractPeer me = getPeer();
		if (sameSubnet(me,them)) {
			Logger.info("Finding local peers: me=" + me.toString() + " them=" + them.toString());
		
			try {
				AbstractPeer dest = AbstractFactory.getFactory().getMulticastPeer();
				_usm.send(dest, DMT.createWhoAreYou());
			}
			catch(Exception e) {
				Logger.warning("Exception occurred while creating WebPeer!", e);
			}
			
		}		
	}

	public AbstractPeer getPeer() {
		return _peer;
	}
	
	public abstract boolean isBadAddress(AbstractPeer peer);

	public void setPeer(AbstractPeer peer) {
		if (!isBadAddress(peer)) {
			_peer = peer;
		}
		//_peer = peer;
	}

	public ArrayList getPeers() {
		return _peers;
	}

	public int getMaxSize() {
		return _maxSize;
	}

	public Map getLastContact() {
		return _lastContact;
	}

	public int getNewestSeenBuild() {
		return _newestSeenBuild;
	}

	public boolean isUserNotifiedOfNewsestSeenBuild() {
		return _userNotifiedOfNewsestSeenBuild;
	}

	public void setUserNotifiedOfNewsestSeenBuild(boolean notifiedOfNewsestSeenBuild) {
		_userNotifiedOfNewsestSeenBuild = notifiedOfNewsestSeenBuild;
	}

	public void setPeerInfo(AbstractPeer p, PeerInfo pi) {
		try {
			Loader.reload(pi.getBuild(), false);
		} catch (Exception e) {
			Logger.error(e);
		}
		if (pi.getBuild() > _newestSeenBuild) {
			Logger.info("Newest seen build now " + pi.getBuild());
			_newestSeenBuild = pi.getBuild();
			_userNotifiedOfNewsestSeenBuild = false;
		}
		if ((Dijjer.FIRST_GOOD_BUILD > pi.getBuild()) || (Dijjer.BUILD < pi.getFirstGoodBuild())) {
			// Doh, we or it are too old
			LinkedList list = new LinkedList();
			list.add(p);
			removePeers(list, "Too old");
			Logger.info("Removed peer " + p + " because we or it are too old to talk " + "(we are " + Dijjer.BUILD
					+ ", it is " + pi.getBuild() + ")");
		} else {
			_peerInfo.put(p, pi);
		}
	}

	public PeerInfo getPeerInfo(AbstractPeer p) {
		return (PeerInfo) _peerInfo.get(p);
	}

	public static AbstractRoutingTable getRoutingTable() {
		return _rt;
	}

	private class Pinger extends AbstractThread {

		final ArrayList _peerList;
		final int _pingRate;
		final int _multi;

		public Pinger(ArrayList peers, int pingRate, int multiplier) {
			_peerList = peers;
			_pingRate = pingRate;
			_multi = multiplier;
			new Thread() {
				public void run() {
					while(!wasKilled() && !wasShutdown()) {
						cleanupPeers((List) _peerList.clone(), _pingRate * _multi);
						try {
							Thread.sleep(_pingRate * _multi);
						} catch (InterruptedException e) {
						}
					}
				}
			}.start();
		}

		protected boolean loop() throws InterruptedException {
			pingPeers((List) _peerList.clone());
			Thread.sleep(_pingRate);
			return true;
		}

		protected void cleanUp() {
		}

		private void cleanupPeers(List peers, int waitTime) {
			List toRemove = new ArrayList();
			for (Iterator i = peers.iterator(); i.hasNext();) {
				AbstractPeer peer = (AbstractPeer) i.next();
				if (tooLongSinceLastContact(peer, waitTime)) {
					toRemove.add(peer);
				}
			}
			if (!toRemove.isEmpty()) {
				removePeers(toRemove, "Too long since last contact");
			}
		}

		private void pingPeers(List peers) {
			for (Iterator i = peers.iterator(); i.hasNext();) {
				AbstractPeer peer = (AbstractPeer) i.next();
				if (peer == null) {
					continue;
				}
				_usm.send(peer, DMT.createPing());
			}
		}

		private boolean tooLongSinceLastContact(AbstractPeer peer, int rate) {
			Map lc = (Map) _lastContact.clone();
			if (lc.containsKey(peer)) {
				return (System.currentTimeMillis() - ((Long) lc.get(peer)).longValue()) > rate;
			}
			return true;
		}
	}
	
	public static class PeerInfo {

		private final int _build;
		private final int _firstGoodBuild;
		private final AbstractPeer _exernalAddress;

		public PeerInfo(int build, int firstGoodBuild, AbstractPeer exernalAddress) {
			_build = build;
			_firstGoodBuild = firstGoodBuild;
			_exernalAddress = exernalAddress;
		}

		public int getFirstGoodBuild() {
			return _firstGoodBuild;
		}

		public int getBuild() {
			return _build;
		}

		public String toString() {
			return "PeerInfo{" + "_build=" + _build + ", _firstGoodBuild=" + _firstGoodBuild + ", _exernalAddress="
					+ _exernalAddress + "}";
		}
	}
	
	public static interface ChangeListener {
		public void removed(AbstractPeer p);
		public void added(AbstractPeer p);
	}
}
